package tool.expand;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;

public class MacroExpander {
	private static ObjectMapper mapper = new ObjectMapper(); // can reuse, share globally
	private ScopedMap<String, Object> scopedMap = new ScopedMap<String, Object>();
	private HashMap<String, Element> macros = new HashMap<String, Element>();
	
	public MacroExpander(Map<String, Object> map) {
		scopedMap.addScope(map);
	}
	public MacroExpander() {
		this(new HashMap<String, Object>());
	}
	public void expand(PrintWriter os, Document doc) throws IOException {
		Element root = doc.getRootElement();
		if (!"script".equals(root.getName())) {
			throw new IllegalArgumentException("Root Element must be 'script'");
		}
		List<Node> content = root.content();
		expand(os, content);
	}
	static final int SET = "set".hashCode();
	private void expand(PrintWriter os, Element elem) throws IOException {
		if ("set".equals(elem.getName())) {
			expandSet(elem.getText());
		} else if ("for".equals(elem.getName())) {
			expandFor(os, elem);
		} else if ("if".equals(elem.getName())) {
			expandIf(os, elem);
//		} else if ("while".equals(elem.getName())) {
//			expandWhile(os, elem);
		} else if ("macro".equals(elem.getName())) {
			expandMacro(elem);
		} else if ("run".equals(elem.getName())) {
			expandRun(os, elem);
		} else {
			throw new IllegalArgumentException("Element '" + elem.getName() + "' not allowed");
		}
	}

	private void expandRun(PrintWriter os, Element elem) throws IOException {
		String name = expand(elem.attributeValue("name"));
		if (name == null) {
			throw new IllegalArgumentException("run is missing 'name' attribute");
		}
		Element macroElem = macros.get(name);
		if (macroElem == null) {
			throw new IllegalArgumentException("Unknown macro name: '" + name + "'");
		}
		List<Node> content = macroElem.content();
		expand(os, content);
		
	}
	private void expandMacro(Element elem) {
		String name = expand(elem.attributeValue("name"));
		if (name == null) {
			throw new IllegalArgumentException("macro is missing 'name' attribute");
		}
		macros .put(name, elem);
		
	}
// not useful (may loop forever) until one can manipulate an outer scope	
//	private void expandWhile(PrintWriter os, Element elem) throws IOException {
//		String cond = expand(elem.attributeValue("true"));
//		if (cond == null) {
//			throw new IllegalArgumentException("while is missing 'true' attribute");
//		}
//		List<Node> content = elem.content();
//		while (isTrue(cond)) {
//			expand(os, content);
//		}
//	}
	private void expandIf(PrintWriter os, Element elem) throws IOException {
		String cond = expand(elem.attributeValue("true"));
		if (cond == null) {
			throw new IllegalArgumentException("if is missing 'true' attribute");
		}
		List<Node> content = elem.content();
		if (isTrue(cond)) {
			expand(os, content);
		}
	}
	private static boolean isTrue(String cond) {
		if ("true".equals(cond.toLowerCase())) return true;
		if ("false".equals(cond.toLowerCase())) return false;
		String[] p;
		p = split(cond, "==");
		boolean result;
		if (p != null) {
			result = p[0].compareTo(p[1]) == 0;
			return result;
		}
		p = split(cond, "!=");
		if (p != null) {
			result = p[0].compareTo(p[1]) != 0; 
			return result;
		}
		p = split(cond, "<=");
		if (p != null) {
			result = p[0].compareTo(p[1]) <= 0; 
			return result;
		}
		p = split(cond, ">=");
		if (p != null) {
			result = p[0].compareTo(p[1]) >= 0; 
			return result;
		}
		p = split(cond, "<");
		if (p != null) {
			result = p[0].compareTo(p[1]) <  0; 
			return result;
		}
		p = split(cond, ">");
		if (p != null) {
			result = p[0].compareTo(p[1]) >  0; 
			return result;
		}
		throw new IllegalArgumentException("Illegal conditional expression: '" + cond + "'");
	}
	private static String[] split(String cond, String op) {
		int ndx = cond.indexOf(op);
		if (ndx<0) return null;
		String a = cond.substring(0,ndx).trim();
		String b = cond.substring(ndx + op.length()).trim();
		return new String[]{a,b};
	}
	private void expandSet(String s) throws JsonParseException, JsonMappingException, IOException {
		StringReader is = new StringReader(s);
		HashMap<String,Object> props = mapper.readValue(is, HashMap.class);
		scopedMap.putAll(props);
	}
	private void expandFor(PrintWriter os, Element elem) throws IOException {
		String var = elem.attributeValue("var");
		if (var == null) {
			throw new IllegalArgumentException("for is missing 'var' attribute");
		}
		String in =  elem.attributeValue("in");
		if (in == null) {
			throw new IllegalArgumentException("for is missing 'in' attribute");
		}
		Object inVal = scopedMap.get(in);
		List<String> values = (List<String>) inVal; 
		scopedMap.addScope();
		List<Node> content = elem.content();
		for (String value : values) {
			scopedMap.put(var, value);
			expand(os, content);
		}
		scopedMap.removeScope();
	}
	private void expand(PrintWriter os, List<Node> content) throws IOException {
		for (Node child : content) {
			switch (child.getNodeType()) {
			case Node.TEXT_NODE:
				os.print(expand(child.getText()));
				break;
			case Node.ELEMENT_NODE:
				expand(os, (Element)child);
				break;
			}
		}
	}
	private String expand(String text) {
		String result = Expander.expand(text, scopedMap);
		if (result.trim().length()==0) {
			result = "";
		}
		return result;
	}
	/**
	 * @param args
	 * @throws DocumentException 
	 * @throws IOException 
	 * @throws JsonMappingException 
	 * @throws JsonParseException 
	 */
	public static void main(String[] args) throws DocumentException, JsonParseException, JsonMappingException, IOException {
        HashMap<String,Object> props = mapper.readValue(new File("src/data/props.json"), HashMap.class);
        MacroExpander exp = new MacroExpander(props);

        SAXReader reader = new SAXReader();
        Document document = reader.read(new File("src/data/script1.xml"));
        PrintWriter os = new PrintWriter(new FileWriter("src/data/script1.txt"));
        exp.expand(os, document);
        os.close();
	}
	
}
